// 
//  Copyright © 2009 Jiří Zárevúcky <zarevucky.jiri@gmail.com>
// 
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Affero General Public License as
//  published by the Free Software Foundation, either version 3 of the
//  License, or (at your option) any later version.
// 
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Affero General Public License for more details.
// 
//  You should have received a copy of the GNU Affero General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
// 
// 


using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Globalization;
using System.Runtime.CompilerServices;


using Anculus.Core;
using Galaxium.Protocol.Xmpp.Library.Utility;
using Galaxium.Protocol.Xmpp.Library.Xml;
using Galaxium.Protocol.Xmpp.Library.Core;

// TODO: honor rfc3920bis, section 4.5 Reconnection
// TODO: section 5.5 - Stream features of rfc3920bis
// TODO: section 5.7
// TODO: support handling of see-other-host stream error
// TODO: attempt reconnect on tls failure

namespace Galaxium.Protocol.Xmpp.Library.Core
{
	public delegate void QueryHandler (Iq iq);
	
	public abstract class CoreStream
	{
		private static readonly string _stream_header;
		
		static CoreStream ()
		{
			_stream_header = "<?xml version='1.0' encoding='UTF-8'?>";
			_stream_header += "<stream:stream ";
			_stream_header += "version='1.0' ";
			_stream_header += "xmlns='" + Namespaces.Client + "' ";
			_stream_header += "xmlns:stream='" + Namespaces.Stream + "' ";
			_stream_header += "xml:lang='" + CultureInfo.CurrentCulture.IetfLanguageTag + "' ";
			_stream_header += "to='{0}'>";

			// according to xmpp-core-bis there should also be a "from" attribute,
			// but that would send user's identity over an unsecure channel
		}
		
		private StreamParser _parser;

		private Timer _ping_timer;
		private const int PING_TIME = 60 * 1000;
		
		#region Properties

	//	public int TlsTimeout         { get; set; }
	//	public int FeaturesTimeout    { get; set; }
		
		public Version        Version    { get; private set; }
		public string         ID         { get; private set; }
		public StreamFeatures Features   { get; private set; }

		public ConnectionState ConnectionState { get; private set; }
		
		public JabberID UID { get; private set; }
		public string Password { private get; set; }
		
		#endregion
		
		public event EventHandler<StanzaEventArgs> StanzaReceived;
		public event EventHandler<StanzaEventArgs> StanzaSent;

		public event EventHandler<ConnectionStateEventArgs> ConnectionStateChanged;
		
		protected internal event EventHandler<StreamErrorEventArgs> StreamError;
		
		private Action<Element> _element_handler;
		
		#region Event triggers
		
		private void OnStanzaReceived (Stanza stanza)
		{
			if (StanzaReceived != null)
				StanzaReceived (this, new StanzaEventArgs (stanza));
		}
		
		private void OnStanzaSent (Stanza stanza)
		{
			if (StanzaSent != null)
				StanzaSent (this, new StanzaEventArgs (stanza));
		}
		
		private void OnStreamError (StreamError error)
		{
			if (StreamError != null)
				StreamError (this, new StreamErrorEventArgs (error));
		}
		
		private void OnConnectionStateChanged (ConnectionState state)
		{
			ConnectionState = state;
			if (ConnectionStateChanged != null)
				ConnectionStateChanged (this, new ConnectionStateEventArgs (state));
		}
		
		#endregion

		private Connection _connection;
		
		public CoreStream (JabberID uid)
		{
			UID = uid;
			
			this.ConnectionStateChanged += (s, e) => {
				if (e.State == ConnectionState.Disconnected)
					ClearWaiters ();
			};
		}
		
		private void HandleElementParsed (object sender, ElementEventArgs e)
		{
			var elm = e.Element;
			
			switch (elm.Name) {
				case "error":
					if (elm.Prefix == "stream")
						OnStreamError (new StreamError (elm));
					break;
				case "iq":
					var iq = new Iq (elm);
					OnStanzaReceived (iq);
					HandleIQ (iq);
					break;
				case "presence":
					var pres = new Presence (elm);
					OnStanzaReceived (pres);
					foreach (var entry in _presence_listeners)
						if (entry.Listener.HandlePresence (pres)) break;
					break;
				case "message":
					var msg = XmlMessage.Create (elm);
					OnStanzaReceived (msg);
					foreach (var entry in _message_listeners)
						if (entry.Listener.HandleMessage (msg)) break;
					break;
				default:
					if (_element_handler != null)
						_element_handler (elm);
					break;
			}
		}
		
		#region XMPP Ping
		
		private bool Ping (int timeout)
		{
			try {
				var query = new Iq (IqType.Get, "ping", Namespaces.Ping);
				SendQuery (query, timeout);
			}
			catch (QueryException e) {
				Log.Warn ("Error in response to ping: " + e.Stanza.Error.GetHumanRepresentation ());
				if (e.Condition == "timed-out" ||
				    e.Condition == "disconnected") return false;
			}
			catch (Exception e) {
				Log.Error (e, "Exception occured while pinging.");
				return false;
			}
			
			return true;
		}

		private void DoPing (object obj)
		{
			if (Ping (60 * 1000)) {
				_ping_timer.Change (PING_TIME, -1);
			}
			else {
				Log.Error ("XMPP ping unsuccessful, connection with server severed");
				Disconnect ();
				_ping_timer.Dispose ();
				_ping_timer = null;
			}
		}
		
		private void InitPing ()
		{
			_ping_timer = new Timer (DoPing, null, PING_TIME, -1);
		}
		
		#endregion
				
		#region IQ handling
				
		private Dictionary<string, QueryHandler> _set_handlers =
			new Dictionary<string, QueryHandler> ();
		private Dictionary<string, QueryHandler> _get_handlers =
			new Dictionary<string, QueryHandler> ();

		private Dictionary<string, QueryHandler> _response_handlers =
			new Dictionary<string, QueryHandler> ();
		
		private void HandleIQ (Iq iq)
		{
			switch (iq.Type) {
			case IqType.Get:
			case IqType.Set:
				HandleQuery (iq);
				break;
			case IqType.Result:
			case IqType.Error:
				HandleResponse (iq);
				break;
			}
		}

		private void HandleResponse (Iq response)
		{
			// TODO: check the source JID (?)
			
			var id = response.ID;
			if (id == null) return;
			QueryHandler handler;
			if (_response_handlers.TryGetValue (id, out handler)) {
				handler (response);
				_response_handlers.Remove (id);
			}
		}
		
		private void HandleQuery (Iq query)
		{
			Dictionary<string, QueryHandler> handlers;

			handlers = (query.Type == IqType.Get) ? _get_handlers : _set_handlers;
			
			try {
				if (handlers.ContainsKey (query.QueryNamespace) && CanQuery (query.From)) {
					handlers [query.QueryNamespace] (query);
				}
				else throw new QueryException ("cancel", "service-unavailable", null);
			}
			catch (QueryException e) {
				var reply = new Iq (IqType.Error);
				reply.To = query.From;
				reply.ID = query.ID;
				reply.AppendChild (e.ToXml ());
				Send (reply);
			}
		}
		
		private void ClearWaiters ()
		{
			foreach (var pair in _response_handlers) {
				var reply = new Iq (IqType.Error);
				reply.ID = pair.Key;
				var error = "You got disconnected while processing the request";
				reply.AppendError (new StanzaError ("disconnected", error));
				pair.Value.Invoke (reply);
			}
			_response_handlers.Clear ();
		}
		
		public void SendWithId (Stanza stanza)
		{
			stanza.ID = stanza.ID ?? IdGen.GetID ();
			Send (stanza);
		}

		public string SendQuery (Iq query, QueryHandler handler)
		{
			var id = query.ID;
			if (id == null)
				query.ID = id = IdGen.GetID ();
			_response_handlers [id] = handler;
			Send (query);
			return id;
		}
		
		private void FakeErrorReply (string id, string condition, string description)
		{
			var reply = new Iq (IqType.Error);
			reply.ID = id;
			reply.AppendError (new StanzaError (condition, description));
			HandleResponse (reply);
		}
		
		public bool DiscardQuery (string id)
		{
			return _response_handlers.Remove (id);
		}
		
		public Iq SendQuery (Iq query, int timeout)
		{
			var waiter = new ManualResetEvent (false);
			Iq result = null;

			var id = SendQuery (query, (response) => {
				result = response;
				waiter.Set ();
			});

			try {
				if (!waiter.WaitOne (timeout, false))
					FakeErrorReply (id, "timed-out", "Request timed out.");
			}
			catch (ThreadAbortException) {
				DiscardQuery (id);
			}

			if (result.IsError)
				throw new QueryException (result);

			return result;
		}

		public void DefineSetQueryHandler (string xmlns, QueryHandler handler)
		{			
			if (handler == null) _set_handlers.Remove (xmlns);
			else _set_handlers [xmlns] = handler;
		}
		
		public void DefineGetQueryHandler (string xmlns, QueryHandler handler)
		{
			if (handler == null) _get_handlers.Remove (xmlns);
			else _get_handlers [xmlns] = handler;
		}
		
		#endregion
		
		#region Stanza listeners
		
		private struct MessageListenerEntry
		{
			public IMessageListener Listener;
			public byte Priority;
		}
		
		private struct PresenceListenerEntry
		{
			public IPresenceListener Listener;
			public byte Priority;
		}
		
		private List<MessageListenerEntry> _message_listeners =
			new List<MessageListenerEntry> ();
		
		private List<PresenceListenerEntry> _presence_listeners =
			new List<PresenceListenerEntry> ();
		
		public void AttachMessageListener (IMessageListener listener, byte priority)
		{
			var entry = new MessageListenerEntry () {
				Listener = listener, Priority = priority
			};
			_message_listeners.Add (entry);
			_message_listeners.Sort ((a, b) => b.Priority.CompareTo (a.Priority));
		}
		
		public void DetachMessageListener (IMessageListener listener)
		{
			_message_listeners.RemoveAll ((entry) => Object.ReferenceEquals (listener, entry.Listener));
		}
		
		public void AttachPresenceListener (IPresenceListener listener, byte priority)
		{
			var entry = new PresenceListenerEntry () {
				Listener = listener, Priority = priority
			};
			_presence_listeners.Add (entry);
			_presence_listeners.Sort ((a, b) => b.Priority.CompareTo (a.Priority));
		}
		
		public void DetachPresenceListener (IPresenceListener listener)
		{
			_presence_listeners.RemoveAll ((entry) => Object.ReferenceEquals (listener, entry.Listener));
		}
		
		#endregion
		
		private void HandleStreamStarted (object sender, ElementEventArgs e)
		{
			if (e.Element.Name != "stream") return;
			ID = e.Element ["id"];
			Version = new Version (e.Element ["version"] ?? "0.9");
		}
	
		private void HandleStreamEnded (object sender, EventArgs e)
		{
			Disconnect ();
		}
		
		private void ProcessInput ()  // throws ConnectionException
		{
			try {
				if (_connection == null || !_connection.Connected)
					throw new ConnectionException ("Socket was closed.");
				var input = _connection.Receive ().ToCharArray ();
				if (input.Length == 0) throw new ConnectionException ("Socket was closed.");
				if (_ping_timer != null) _ping_timer.Change (PING_TIME, -1);
				try { _parser.ParseBuffer (input); return; }
				catch (Exception e) { Log.Error (e, "Parser error"); }
			}
			catch (Exception e) { Log.Error (e, "Error while receiving data"); }
			
			Disconnect ();
			throw new ConnectionException ("Error in the incoming data processing");
		}
		
		private void InitLoop ()
		{
			// process incoming data indefinitely in a separate thread until exception occurs
			var thread = new Thread (()=>{try{while(true){ProcessInput();}}catch{}});
			thread.IsBackground = true;
			thread.Start ();
		}
				
		#region Login

		private void InitStream () // throws ConnectionException
		{
			ID = null; Version = null; Features = null;
			
			if (_parser == null) {
				_parser = new StreamParser ();
				_parser.StreamStarted += HandleStreamStarted;
				_parser.StreamEnded += HandleStreamEnded;;
				_parser.ElementParsed += HandleElementParsed;
			} else {
				_parser.Restart ();
			}

			var request = String.Format (_stream_header, UID.Domain);
			var response = SendAndWait (request); //, FeaturesTimeout);
			
			if (Version == null)
				throw new ConnectionException ("Invalid stream header.");
			
			if (Version.Major != 1)
				throw new ConnectionException ("Unsupported version");
			
			// FIXME: check for namespace, not prefix
			if (response.Prefix != "stream" || response.Name != "features")
				throw new ConnectionException ("Invalid stream features.");		
			
			Features = new StreamFeatures (response);
		}
		
		private void InitEncryption ()
			// throws ConnectionException, AuthenticationException
			//        InvalidOperationException if the connection got terminated
		{
			if (!Features.SupportsTLS) {
				if (ConfirmUnsecuredConnection ()) return;
				throw new ConnectionException ("Connecting aborted for security reasons");
			}

			var elm = SendAndWait (new Element ("starttls", Namespaces.Tls));

			if (elm.Namespace == Namespaces.Tls && elm.Name == "proceed")
				_connection.EnableTLS ();
			else
				throw new AuthenticationException ("TLS negotiation failed.");
			
			InitStream ();
		}
	
		private void Authenticate (string password)
			// throws SaslError, LoginCancelledError
		{
			var auth = new Authenticator (this, HandlePasswordRequest, SendAndWait);
			auth.Authenticate (UID.Bare (), password);
			InitStream ();
		}
		
		private void BindResource ()
			// throws QueryException, LoginCancelledError
		{
			var binder = new ResourceBinder (this);
			
			do {
				try {
					UID = binder.BindResource (UID.Resource ?? Uuid.GenerateRandom ());
					return;
				}
				catch (QueryException e) {
					if (e.Condition == "bad-request")
						UID = new JabberID (UID.Node, UID.Domain, HandleInvalidResource (UID));
					else if (e.Condition == "conflict")
						UID = new JabberID (UID.Node, UID.Domain, HandleResourceConflict (UID));
					else throw;
				}
			} while (UID.Resource != null);

			throw new LoginCancelledException ();
		}
		
		#endregion
		
		public void Connect ()
		{
			try {
				if (ConnectionState != ConnectionState.Disconnected)
					throw new ConnectionException ("Stream is already connecting or online.");
				
				if (UID == null)
					throw new ConnectionException ("Trying to connect without Jabber ID set.");
				
				_connection = new Connection (UID, ConfirmCertificate);
				
				OnConnectionStateChanged (ConnectionState.Connecting);
				_connection.Connect ();
				
				OnConnectionStateChanged (ConnectionState.StreamInit);
				InitStream ();
				
				OnConnectionStateChanged (ConnectionState.EncryptInit);
				InitEncryption ();
				
				OnConnectionStateChanged (ConnectionState.Authenticating);
				Authenticate (Password);
				
				InitLoop ();  // initiate loop receiving
				
				OnConnectionStateChanged (ConnectionState.Binding);
				BindResource ();
				
				OnConnectionStateChanged (ConnectionState.Connected);
				
				_connection.InitKeepAlive ();
				
				InitPing ();
			}
			catch (Exception e) {
				Log.Error (e, "Exception thrown while connecting.");
				Disconnect ();
				throw;
			}
		}
		
		public void Disconnect ()
		{
			if (_connection == null) return;
			
			if (_connection.Connected) {
				_connection.Send ("</stream:stream>");
				_connection.Disconnect ();
			}
			
			_connection = null;
			
			OnConnectionStateChanged (ConnectionState.Disconnected);
		}
		
		public void Send (Element elm)
		{
			if (elm is Stanza)
				OnStanzaSent (elm as Stanza);
			
			_connection.Send (Serializer.Serialize (elm));
		}
		
		private Element SendAndWait (Element elm)  // throws ConnectionException
		{
			return SendAndWait (Serializer.Serialize (elm));
		}
		
		private Element SendAndWait (string data)  // throws ConnectionException
		{
			_connection.Send (data);
			
			var elm = (Element) null;
			_element_handler = (r) => { elm = r; };
			try {
				while (elm == null) ProcessInput ();
			}
			finally {
				_element_handler = null;
			}
			
			return elm;
		}
	
		
		protected abstract bool CanQuery (JabberID uid);
		
		protected abstract bool ConfirmUnsecuredConnection ();
		
		protected abstract bool ConfirmCertificate (object sender,
		                                            X509Certificate certificate,
		                                            X509Chain chain,
		                                            SslPolicyErrors sslPolicyErrors);
		
		protected abstract string HandlePasswordRequest ();
		protected abstract string HandleInvalidResource (JabberID uid);
		protected abstract string HandleResourceConflict (JabberID uid);
	}
}